package cn.scs.client;

import cn.scs.common.Config;
import cn.scs.common.DataOpCode;
import cn.scs.common.FileInfo;
import java.nio.charset.Charset;
import cn.scs.component.DatanodeFinder;
import cn.scs.component.FileReader;
import cn.scs.component.FileWriter;
import cn.scs.impl.DataServer;
import java.nio.CharBuffer;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import java.io.*;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;

public class DistributedFileSystemClient {
    private static final Logger log = LogManager.getLogger(DataServer.class);
    private final MetaServerClient metaDataClient;
    private final DatanodeFinder datanodeFinder;
    //    private final List<StorageNode> storageNodes;
    private String cur_dir = "/";

    public DistributedFileSystemClient() throws IOException {

        metaDataClient = new MetaServerClient();
        //初始化所有slave节点IP信息
        datanodeFinder = new DatanodeFinder("datanodes.properties");
//        storageNodes = new ArrayList<>();
        // 初始化存储节点列表
//        storageNodes.add(new StorageNode("storage-node-1"));
        //storageNodes.add(new StorageNode("storage-node-2"));
        // ...
    }

    // 更新MetaClient meta的掉线重连函数
    public void updateMetaClient() {
        try {
            metaDataClient.updateConnection();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void disconnect() throws IOException {
        // 客户端连接关闭操作
        metaDataClient.close();
    }

    public DataInputStream openFile(String path) {
        // 实现打开文件的逻辑
        // 返回流 表示文件打开成功，返回 null 表示失败
        /*1.创建文件对象*/
        //获取本地项目目录
        String projectPath = System.getProperty("user.dir");
        String filePath = projectPath + path;
        File file = new File(filePath);
        /*2.创建字节输入流*/
        if(file.exists()) {//当file存在时创建输入流
            InputStream in;
            try {
                in = Files.newInputStream(file.toPath());//使用子类FileInputStream建立 二进制输入流
                return new DataInputStream(in);
            } catch (IOException e) {//使用IOException可以解决两种异常
                e.printStackTrace();
            }
        }
        else{
            System.out.println("file dose not exist");
        }
        return null;
    }

    public byte[] readFile(String path, long offset, int len) {
        // 实现从文件读取数据的逻辑
        try {
            // 向元数据服务器发送新建文件请求，返回fileId
            // fileId格式: datanode节点名称：UUID
            // 例如： datanode1:b9a8012c-3db1-4a62-8d1d-cedd48070690
            String fileId = metaDataClient.createFile(path);

            // 创建套接字连接到数据服务器
            // 此处使用的host与元数据服务器相同是因为slave节点现在跟元数据节点在同一主机上
            Connection connection = new Connection(Config.META_SERVRE_HOST, Config.DATA_SERVRE_PORT);

            DataOpCode.READ_FILE.write(connection.getOut());//0.发送写文件的OPCode
            connection.writeUTF(fileId);//1.发送文件ID
            connection.writeLong(offset);//2.发送读取偏移量
            connection.writeInt(len);//2.发送读取长度
            connection.flush();
            int retCode = connection.readInt();//3.回写返回码
            String msg = connection.readUTF();//4.回写消息
            byte[] finalBuffer = new byte[61858764];
//            byte[] buffer;
            int bytesRead = connection.readInt();
            while (bytesRead > 0){
//                buffer = new byte[connection.readInt()];
                //将读到的数据存到buffer数组里
                for (int i = 0; i < bytesRead; i++){
                    finalBuffer[i] = connection.read();
                }
                bytesRead = connection.readInt();
            }

            if(retCode == 0){
                log.info("写入成功，"+msg);
            }else{
                log.error("写入失败，"+msg);
            }
            //关闭连接
            connection.close();
            return finalBuffer;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将data数据写至分布式文件系统路径path
     * @param path
     * @param data
     * @return
     */
    public boolean writeFile(String path, byte[] data) {
        try {
            // 向元数据服务器发送新建文件请求，返回fileId
            // fileId格式: datanode节点名称：UUID
            // 例如： datanode1:b9a8012c-3db1-4a62-8d1d-cedd48070690
            String fileId = metaDataClient.createFile(path);

            System.out.println(fileId);
            // 创建套接字连接到数据服务器
            // 此处使用的host与元数据服务器相同是因为slave节点现在跟元数据节点在同一主机上
            Connection connection = new Connection(Config.META_SERVRE_HOST, Config.DATA_SERVRE_PORT);
            System.out.println("连接成功？");
            DataOpCode.WRITE_FILE.write(connection.getOut());//0.发送写文件的OPCode
            connection.writeUTF(fileId);//1.发送文件ID
            connection.write(data);//2.遍历写入

            int retCode = connection.readInt();//3.回写返回码
            String msg = connection.readUTF();//4.回写消息
            if(retCode == 0){
                log.info("写入成功，"+msg);
            }else{
                log.error("写入失败，"+msg);
            }
            //关闭连接
            connection.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    public boolean closeFile(DataInputStream dataInputStream) {
        // 实现关闭文件的逻辑
        try{
            dataInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    private void listFiles(String path) {
        List<String> fileNameList = metaDataClient.listFile(path); // 获取文件列表方法

        if (fileNameList.isEmpty() || fileNameList.get(0).equals("empty directory")) {
            System.out.println("No files found.");
        } else {
            System.out.println("Files in the system:");
            for (String file : fileNameList) {
                System.out.println(file);
            }
        }
    }

    // 获取文件大小
    private long getFileSize(String filePath) {
        // 实现获取文件大小的逻辑
        return 0; // 假设获取文件大小的方法
    }

    // 新建文件
    private String createFile(String localPath, String filePath) {  // -HUADD
        // 检测文件是否存在
        if (! new File(localPath).exists()) {
            System.out.println("File not Found in local!");
            return " : ";
        }
        try {
            // 请删除测试用 已分发的数据块计数变量 n
            int n = 1;
            // 向元数据服务器发送新建文件请求，返回fileId
            // fileId格式: datanode节点名称：UUID
            // 例如： datanode1:b9a8012c-3db1-4a62-8d1d-cedd48070690
            String nodeAndFileId = metaDataClient.createFile(filePath);
            if (nodeAndFileId == null) {
                // 返回null 表示失败
                System.out.println("File Existed!");
                return " : ";
            }
            //得到uuid作为文件名,取出nodeName来获取对应Ip
            String nodeName = nodeAndFileId.split(":")[0];
            String fileId = nodeAndFileId.split(":")[1];
            // 循环读取数据块 分发UUID 及数据块 至slave
            FileReader reader = new FileReader(localPath);
            byte[] data = reader.readBlock(Config.blockSize);
            // 当reader读取完毕后 data将被返回null 赋值
            while (data != null) {

                //选择datanode主机Ip连接
                String danaNodeIp = datanodeFinder.getDatanodeIP(nodeName);
                int dataNodePort = datanodeFinder.getDatanodePort(nodeName);

                // 创建套接字连接到数据服务器DataServer
                Connection connection = new Connection(danaNodeIp, dataNodePort);
                DataOpCode.CREATE_FILE.write(connection.getOut()); //向服务器发送 操作码code
                connection.flush();
                connection.writeUTF(fileId); // 读取客户端发送fileId
                connection.writeUTF(Config.USER);
                connection.writeBoolean(false);     // 是否为目录
                // 因为数据byte流不稳定，无法一次性发送，先发送声明byte数组尺寸
                connection.writeInt(data.length);
                // 发送数据
                connection.write(data);
                connection.flush();
                // 接收操作返回信息 并关闭socket流
                int code = connection.readInt();
                String info = connection.readUTF();
                connection.close(); // 关闭
                // 更新输出信息
                System.out.println("\rDistributed Blocks:\t"+ n +"\t\t"+fileId+"\t\t"+ data.length / 1024 + " Kb");
                n += 1;
                // 一次Block分发循环完毕，更新UUID和Slave Host，预备进入下一次循环
                data = reader.readBlock(Config.blockSize);  // 更新数据块
                if (data == null) {
                    break;  // 即使终止，防止后续申请无用数据块
                }
                nodeAndFileId = metaDataClient.applyBlock(filePath, nodeAndFileId); // 申请获取新块的nodeAndFileId
                fileId = nodeAndFileId.split(":")[1];   // 更新fileId
                nodeName = nodeAndFileId.split(":")[0]; //更新新dataNode
            }
            System.out.println();   // 换行保留块结果
            reader.closeStream();   // 关闭本地文件读取流
            //返回节点存储信息
            return nodeAndFileId;
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(new Date() +" createFile done." );
        return " : "; // 无创建返回空字符

    }
    // 下载文件
    public boolean downloadFile(String remoteFilePath, String localFilePath) {
        try {
            // 请删除测试用 已搜集的数据块计数变量 n
            int n = 1;
            List<String> locations = metaDataClient.downloadFile(remoteFilePath);
            // 新建写入文件对象
            FileWriter writer = new FileWriter(localFilePath);
            // 依次遍历块请求 写入文件
            for (String loc:locations) {

                //选择datanode主机Ip连接
                String danaNodeIp = datanodeFinder.getDatanodeIP(loc.split(":")[0]);
                int dataNodePort = datanodeFinder.getDatanodePort(loc.split(":")[0]);

                Connection connection = new Connection(danaNodeIp, dataNodePort);
                DataOpCode.DOWNLOAD_FILE.write(connection.getOut());
                connection.writeUTF(loc.split(":")[1]); // 传输UUID
                connection.writeUTF(Config.USER); // 传输USER
                byte[] block = readByte(connection.getIn());
                //文件块总大小
                writer.write(block, true);  // 追加写入
                connection.close(); // 关闭连接
                System.out.println("Collected Blocks:\t"+ n +"\t\t"+loc+"\t\tSize: "+ block.length / 1024 +" kb");
                n+=1;
            }
            writer.closeStream();   // 关闭写入流
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 实现下载文件逻辑
        return false;
    }
    // 删除文件
    public boolean deleteFile(String remoteFilePath) {
        try {
            // 请删除测试用 已搜集的数据块计数变量 n
            int n = 1;
            // 获取locations
            List<String> locations = metaDataClient.deleteFile(remoteFilePath);    // 返回执行结果
            // 依次遍历块请求 删除slave上的文件块
            for (String loc:locations) {

                //选择datanode主机Ip连接
                String danaNodeIp = datanodeFinder.getDatanodeIP(loc.split(":")[0]);
                int dataNodePort = datanodeFinder.getDatanodePort(loc.split(":")[0]);

                Connection connection = new Connection(danaNodeIp, dataNodePort);
                DataOpCode.DEL_FILE.write(connection.getOut());
                connection.writeUTF(loc.split(":")[1]); // 传输UUID
                connection.writeUTF(Config.USER); // 传输USER
                int code = connection.readInt();    // 获取操作情况
                String info = connection.readUTF(); // 获取删除信息
                connection.close(); // 关闭连接
                if (code==0) {
                    // 成功删除一个块
                    System.out.println("Deleted Blocks:\t"+ n +"\t\t"+loc);
                    n+=1;
                }
            }
            System.out.println();   // 换行保留块结果
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
    // 读取in 字节流
    private byte[] readByte(DataInputStream in) throws IOException {
        int size = in.readInt();
//        System.out.print("\t"+String.valueOf(size));
        // 获取size构建byte[]
        byte[] dataBlock = new byte[size];   // 接收区 大小为传入大小
        int pointer = 0;    // 记录偏移
        int res = 0;    // 记录读取结果
        int bufferSize = 8192;    // 每次读8kb 最为保险
        // 循环检测读取block
        while (res != -1 && pointer < size) {
            int readSize = Math.min(bufferSize, size-pointer);
            res = in.read(dataBlock, pointer, readSize);  // 偏移填充, 更新状态
            pointer = pointer + readSize;
        }
        return dataBlock;
    }


    public boolean createDirectory(String remoteDirectoryPath) {
        // 实现创建目录逻辑
        return false;
    }

    public boolean deleteDirectory(String remoteDirectoryPath) {
        // 实现删除目录逻辑
        return false;
    }

    public FileInfo getFileInfo(String remoteFilePath) {
        // 实现获取文件信息逻辑
        return null;
    }
    public boolean copyFile(String sourcePath, String destinationPath) {
        // 实现复制文件逻辑
        return false;
    }
    public boolean moveFile(String sourcePath, String destinationPath) {
        // 实现移动文件逻辑
        return false;
    }

    /*
    java中File类自带一个检测方法exists可以判断文件或文件夹是否存在，
    一般与mkdirs方法（该方法相较于mkdir可以创建包括父级路径，推荐使用该方法）或者createNewFile方法合作使用。
    */
    public boolean checkPathExist(String filepath) {
        List<String> fileList = metaDataClient.listFile(filepath);
        return !fileList.isEmpty();
    }
    // 重命名文件或目录
    private boolean renameFile(String path,String newName){
        return metaDataClient.renameFile(path, newName);
    }

    // 向MetaServer声明 操作完毕，客户端退出，申请储存Image
    private boolean exitClient() {
        return metaDataClient.DFS_Client_exit();
    }

    // 与存储节点断开连接
    private void disconnectFromStorageNodes() {
        // 实现断开连接逻辑
    }
    // 其他辅助方法的实现...
    private String getFileName(String path) {
        // 使用lastIndexOf方法获取最后一个"/"的索引
        int lastSeparatorIndex = path.lastIndexOf("/");
        // 使用substring方法提取文件名
        return path.substring(lastSeparatorIndex + 1);
    }
    private void catDfsFile( String catSavePath){
        /*
         * 这种方式更适用于处理大型文件，因为它使用了缓冲区来减少I/O操作次数
         * */
        Path filePath = Paths.get(catSavePath);
        try (FileChannel fileChannel = FileChannel.open(filePath, StandardOpenOption.READ)) {
            // 分配一个大小为8KB的ByteBuffer来读取文件内容
            ByteBuffer buffer = ByteBuffer.allocate(8 * 1024);

            while (fileChannel.read(buffer) != -1) {
                /*
                  flip ()函数的作用是将写模式转变为读模式，
                  即将写模式下的Buffer中内容的最后位置变为读模式下的limit位置，作为读越界位置，
                  同时将当前读位置置为0，表示转换后重头开始读，同时再消除写模式下的mark标记，代码如下
                  **/
                buffer.flip();

                Charset charset = StandardCharsets.UTF_8;
                CharBuffer charBuffer = charset.decode(buffer);

                // 将CharBuffer中的内容输出到控制台
                while (charBuffer.hasRemaining()) {
                    System.out.print(charBuffer.get());
                }
                //换行
                System.out.println();
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //读完后删除临时文件
        File file = new File(catSavePath);
        if (file.exists()) {
            boolean deleted = file.delete();
            if (deleted) {
                System.out.println("文件读取成功！");
            } else {
                System.out.println("文件读取失败！");
            }
        }
    }
    public static void main(String[] args) throws IOException {
        DistributedFileSystemClient client = new DistributedFileSystemClient();
        //输入流
        Scanner scanner = new Scanner(System.in);
        boolean running = true;
        while (running) {
            System.out.println("Enter command (upload, download, delete, rename, exit, cat, ...):");
            String command = scanner.nextLine();
            // 引入 catch 保证 DFSClient 不会直接退出 ， 可以人为 exit + refresh 重新获取metaServer连接
            try {
                switch (command.toLowerCase()) {
                    case "upload":
                        System.out.println("Enter DFS file path to create:");
                        String createPath = scanner.nextLine();
                        System.out.println("Enter local file path to submit:");
                        String readPath = scanner.nextLine();
                        String nodeAndFileId = client.createFile(readPath, createPath);
//                    String nodeName = nodeAndFileId.split(":")[0];
//                    String localFileId = nodeAndFileId.split(":")[1];
                        //客户端输出数据存储节点信息
//                    System.out.println("文件存储在："+nodeName+":"+localFileId);
                        break;
                    case "download":
                        System.out.println("Enter DFS file path to download:");
                        String downloadPath = scanner.nextLine();
                        System.out.println("Enter Path to save the File:");
                        String savePath = scanner.nextLine();
                        boolean res_down = client.downloadFile(downloadPath, savePath);
                        if (res_down) {
                            System.out.println("文件存储在："+savePath);
                        } else {
                            System.out.println("下载失败");
                        }
                        break;
                    case "delete":
                        System.out.println("Enter file path:");
                        String deletePath = scanner.nextLine();
                        if (!client.checkPathExist(deletePath)){
                            System.out.println("file path not find , delete stopped!");
                            break;
                        }
                        boolean res_del = client.deleteFile(deletePath);
                        if(res_del){
                            System.out.println("File" + deletePath + " has already disappeared");
                        }else System.out.println("File " + deletePath + " delete fail.");
                        break;
                    case "rename":
                        System.out.println("Enter file path:");
                        String path = scanner.nextLine();
                        if (!client.checkPathExist(path)){
                            System.out.println("file path not find , rename stopped!");
                            break;
                        }
                        System.out.println("Enter file newName:");
                        String newName = scanner.nextLine();
                        if(client.renameFile(path,newName)){
                            System.out.println( path + " has already renamed." + "to " + newName);
                        }else System.out.println( path + " rename fail.");
                        break;
//                case "open":
//                    System.out.println("Enter file path:");
//                    String openPath = scanner.nextLine();
//                    DataInputStream fileStream = client.openFile(openPath);
//                    if (fileStream != null) {
//                        System.out.println("File opened.");
//                    } else {
//                        System.out.println("Failed to open file.");
//                    }
//                    break;
                    case "cd":
                        System.out.println("Enter directory path:");
                        String rel_path = scanner.nextLine().trim();
                        if (rel_path.equals(".")){
                            client.cur_dir = "/";
                            break;
                        }
                        //要跳转的路径存在
                        if(client.checkPathExist(rel_path)){
                            client.cur_dir = rel_path;
                            System.out.println("cd to: "+client.cur_dir +" success ");
                        }else {
                            System.out.println("directory " + rel_path + " not exist. ");
                            System.out.println("cd stopped! ");
                            break;
                        }

                        break;
                    case "read":
                        // 实现从文件读取数据的逻辑
                        break;

                    case "write":
                        // 实现向文件写入数据的逻辑
                        System.out.println("Enter file path:");
                        String openPath = scanner.nextLine();
                        DataInputStream fileStream = client.openFile(openPath);
                        if (fileStream != null) {
                            System.out.println("File opened.");
                        } else {
                            System.out.println("Failed to open file.");
                        }
                        System.out.println("Enter data to write:");
                        String data = scanner.nextLine();
                        boolean flag = client.writeFile(openPath,data.getBytes(StandardCharsets.UTF_8));
                        if (flag){
                            System.out.println("write file success");
                        }
                        else {
                            System.out.println("write file fail");
                        }
                        break;

                    case "ls":
                        client.listFiles(client.cur_dir);
                        break;

                    case "cat":
                        System.out.println("Enter DFS file path to cat:");
                        String catPath = scanner.nextLine();
                        String catPathName = client.getFileName(catPath);
                        //临时文件目录
                        String catSavePath = "./tmp/"+ catPathName;
                        System.out.println(catSavePath);
                        client.downloadFile(catPath, catSavePath);
                        //具体读取文件操作
                        client.catDfsFile(catSavePath);

                        break;
                    case "close":
                        // 实现关闭文件的逻辑--关闭当前文件流???

                        break;

                    case "exit":
                        // 向metaServer申请 保存Image 再退出
                        if (client.exitClient()) {
                            System.out.println("DFS Client Exit! Meta data Image has been updated.");
                            running = false;
                        } else {
                            System.out.println("ERROR: Exit Failed! MetaServer Unknown Error happened......");
                        }
                        break;
                    case "refresh":
                        // 重连
                        client.updateMetaClient();
                        break;
                    default:
                        System.out.println("Invalid command.");
                        break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        client.disconnect();
        scanner.close();
    }
}

